/*
 * File     : LibraryBuilder.java
 * Created  : 21 Apr 2011
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.library;

import static com.googlecode.dni.internal.DniInternal.LOGGER;
import static org.objectweb.asm.Opcodes.*;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;

import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.internal.call.JniNativeMethod;
import com.googlecode.dni.internal.string.FastCharset;
import com.googlecode.dni.internal.structure.StructureFieldFactory;
import com.googlecode.dni.internal.structure.StructureFieldFactory.FieldPointerProvider;
import com.googlecode.dni.library.CallingConvention;
import com.googlecode.dni.library.IllegalLibraryException;
import com.googlecode.dni.library.Library;
import com.googlecode.dni.library.Symbol;
import com.googlecode.dni.type.Pointer;


/**
 * <p>
 *  Generates byte-code for implementations of library types.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class LibraryBuilder extends AbstractLibraryBuilder
{

    /** The interface name (in byte-code style). */
    private final String interfaceName;

    private StructureFieldFactory structureFieldFactory;


    /**
     * Creates a new instance.
     *
     * @param type
     *            the library interface type
     * @param implName
     *            the library implementation name
     *
     * @return an instance
     */
    public static LibraryBuilder create( final Class< ? > type,
                                         final String implName )
    {
        if ( !type.isInterface() )
        {
            throw new IllegalLibraryException( "Library must an interface: " + type.getName() );
        }

        if ( type.getTypeParameters().length > 0 )
        {
            throw new IllegalLibraryException( "Library cannot have type parameters: " + type.getName() );
        }

        Library library = type.getAnnotation( Library.class );

        if ( library == null )
        {
            throw new IllegalLibraryException( "Library missing @Library annotation: " + type.getName() );
        }

        CallingConvention conventions = library.callingConvention();
        if ( !conventions.isSupported() )
        {
            throw new IllegalLibraryException( "Unsupported calling conventions: " + type.getName() + ": " + conventions );
        }
        conventions = conventions.getActual();

        return new LibraryBuilder( type,
                                   implName,
                                   library,
                                   conventions,
                                   library.charset() );
    }

    private LibraryBuilder( final Class< ? > type,
                            final String implName,
                            final Library library,
                            final CallingConvention conventions,
                            final String charset )
    {
        super( type, implName, library, conventions, charset );
        this.interfaceName = type.getName().replace( '.', '/' );
    }

    /**
     * Creates a library instance.
     *
     * @return a new instance
     */
    public LibraryRecord build()
    {
        LOGGER.info( "Creating Library implementation for: " + getType().getName() );

        ClassWriter classWriter = new ClassWriter( ClassWriter.COMPUTE_FRAMES );

        classWriter.visit( V1_6,
                           ACC_PUBLIC | ACC_FINAL,
                           getImplName(),
                           null,
                           "java/lang/Object",
                           new String[] { this.interfaceName } );

        List< JniNativeMethod > nativeMethods = new ArrayList< JniNativeMethod >();
        buildInternal( classWriter, false, nativeMethods );

        writeDefaultConstructor( classWriter );

        writeStaticInitialiser( classWriter );

        classWriter.visitEnd();

        byte[] classFile = classWriter.toByteArray();
        ClassLoader classLoader = getType().getClassLoader();
        Class< ? > implClass = DniInternal.defineClass( getImplName(),
                                                        classLoader,
                                                        classFile );

        // force the class to be initialised
        Object instance;
        try
        {
            instance = implClass.newInstance();
        }
        catch ( InstantiationException exception )
        {
            throw new IllegalLibraryException( "Failed to create library", exception );
        }
        catch ( IllegalAccessException exception )
        {
            throw new IllegalLibraryException( "Failed to create library", exception );
        }

        // register all native methods
        for ( JniNativeMethod nativeMethod : nativeMethods )
        {
            nativeMethod.register( implClass );
        }

        return new LibraryRecord( getType(),
                                  instance,
                                  getImplName() );
    }

    @Override
    void createMethods( final DynamicLibrary dynamicLibrary,
                        final ClassWriter classWriter,
                        final List< JniNativeMethod > nativeMethods )
    {
        for ( Method method : getType().getMethods() )
        {
            if ( method.getDeclaringClass() != Object.class )
            {
                try
                {
                    Symbol symbol = method.getAnnotation( Symbol.class );
                    if ( symbol != null )
                    {
                        handleSymbolMethod( dynamicLibrary, classWriter, method );
                    }
                    else
                    {
                        writeFunctionMethod( dynamicLibrary, method, nativeMethods );
                    }
                }
                catch ( Throwable throwable )
                {
                    throw new IllegalLibraryException( "Problem with method: " + method, throwable );
                }
            }
        }

        if ( this.structureFieldFactory != null )
        {
            this.structureFieldFactory.completedMethods();
            this.structureFieldFactory.writeMethods();
        }
    }

    /**
     * Writes a default constructor.
     *
     * @param classWriter
     *            the class writer
     */
    private void writeDefaultConstructor( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_PUBLIC, "<init>", "()V", null, null );
        methodVisitor.visitCode();

        // super();
        methodVisitor.visitVarInsn( ALOAD, 0 );
        methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/Object", "<init>", "()V" );

        if ( this.structureFieldFactory != null )
        {
            this.structureFieldFactory.writeConstructorFragment( methodVisitor );
        }

        // return
        methodVisitor.visitInsn( RETURN );
        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    private void writeStaticInitialiser( final ClassWriter classWriter )
    {
        MethodVisitor methodVisitor = classWriter.visitMethod( ACC_STATIC | ACC_PRIVATE,
                                                               "<clinit>",
                                                               "()V",
                                                               null,
                                                               null );

        writeStaticInitialiserFragmentInternal( methodVisitor );

        if ( this.structureFieldFactory != null )
        {
            this.structureFieldFactory.writeStaticInitialiserFragment( methodVisitor );
        }

        methodVisitor.visitInsn( RETURN );

        methodVisitor.visitMaxs( 0, 0 );
        methodVisitor.visitEnd();
    }

    /**
     * Writes out a symbol method.
     *
     * @param dynamicLibrary
     *            the dynamic library
     * @param classWriter
     *            the class writer
     * @param method
     *            the Java method on the interface
     */
    private void handleSymbolMethod( final DynamicLibrary dynamicLibrary,
                                     final ClassWriter classWriter,
                                     final Method method )
    {
        if ( this.structureFieldFactory == null )
        {
            String prefix = getLibrary().symbolPrefix();
            FieldPointerProviderImpl fieldPointerProvider = new FieldPointerProviderImpl( dynamicLibrary, prefix );
            this.structureFieldFactory = new StructureFieldFactory( classWriter,
                                                                    getImplName(),
                                                                    FastCharset.getInstance( getCharset() ),
                                                                    fieldPointerProvider );
        }

        this.structureFieldFactory.addMethod( method );
    }

    private static final class FieldPointerProviderImpl implements FieldPointerProvider
    {
        private final DynamicLibrary dynamicLibrary;
        private final String prefix;

        FieldPointerProviderImpl( final DynamicLibrary dynamicLibrary,
                                  final String prefix )
        {
            this.dynamicLibrary = dynamicLibrary;
            this.prefix = prefix;
        }

        @Override
        public Pointer getFieldPointer( final Method getter, final String fieldName )
        {
            Symbol symbol = getter.getAnnotation( Symbol.class );

            String symbolName = symbol.name();
            if ( symbolName.isEmpty() )
            {
                symbolName = this.prefix + fieldName;
            }

            LOGGER.fine( "Looking up symbol: " + symbolName );

            Pointer pointer = null;
            try
            {
                pointer = this.dynamicLibrary.getSymbol( symbolName );
            }
            catch ( SymbolNotFoundException exception )
            {
                // be generous...?
                pointer = tryLowerCase( symbolName );
                if ( pointer == null )
                {
                    throw new IllegalLibraryException( "Missing symbol " + symbolName + " for library", exception );
                }
            }

            if ( pointer == null )
            {
                throw new IllegalLibraryException( "Missing symbol " + symbolName + " for library" );
            }

            return pointer;
        }

        private Pointer tryLowerCase( final String symbolName )
        {
            if ( symbolName.isEmpty() || Character.isLowerCase( symbolName.codePointAt( 0 ) ) )
            {
                return null;
            }

            // meh, using UTF-16 for strings :-P

            StringBuilder lowerCase = new StringBuilder();
            int codePoint0 = symbolName.codePointAt( 0 );
            if ( Character.isSupplementaryCodePoint( codePoint0 ) )
            {
                lowerCase.appendCodePoint( Character.toLowerCase( codePoint0 ) );
                lowerCase.append( symbolName, 2, symbolName.length() );
            }
            else
            {
                lowerCase.append( Character.toLowerCase( (char) codePoint0 ) );
                lowerCase.append( symbolName, 1, symbolName.length() );
            }

            LOGGER.fine( "Looking up symbol: " + lowerCase );

            try
            {
                String newSymbolName = lowerCase.toString();
                return this.dynamicLibrary.getSymbol( newSymbolName );
            }
            catch ( SymbolNotFoundException exception )
            {
                return null;
            }
        }
    }

}
